package own.stu.jobgib.playown.io.common;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;

import static own.stu.jobgib.playown.io.common.SocketOptionTest.Type;

public class TSocket {

    private static final Logger LOGGER = LoggerFactory.getLogger(TSocket.class.getName());

    private Socket socket_;

    private String host_;

    private int port_;

    //Socket timeout - read timeout on the socket
    private int socketTimeout_;

    //Connection timeout
    private int connectTimeout_;

    protected InputStream inputStream_ = null;

    protected OutputStream outputStream_ = null;

    private SimpleProtocol protocol;

    private SocketOptionTest.Type type_;

    public TSocket(Socket socket, Type type) {
        socket_ = socket;
        type_ = type;
        setOptions(socket_);

        if (isOpen()) {
            try {
                inputStream_ = new BufferedInputStream(socket_.getInputStream());
                outputStream_ = new BufferedOutputStream(socket_.getOutputStream());
            } catch (IOException iox) {
                close();
                throw new RuntimeException("not open", iox);
            }
        }

        protocol = new SimpleProtocol(inputStream_, outputStream_);
    }

    public TSocket(String host, int port, Type type) {
        this(host, port, 0, type);
    }

    public TSocket(String host, int port, int timeout, Type type) {
        this(host, port, timeout, timeout, type);
    }

    public TSocket(String host, int port, int socketTimeout, int connectTimeout, Type type) {
        host_ = host;
        port_ = port;
        socketTimeout_ = socketTimeout;
        connectTimeout_ = connectTimeout;
        type_ = type;
        initSocket();
    }

    private void initSocket() {
        socket_ = new Socket();
        setOptions(socket_);
    }

    private void setOptions(Socket socket_) {
        try {
            socket_.setSoLinger(false, 0);
            socket_.setTcpNoDelay(true);
            socket_.setKeepAlive(true);
            socket_.setSoTimeout(socketTimeout_);
            SocketOptionTest.built(socket_, type_.getCode());
        } catch (SocketException e) {
            LOGGER.error("Could not configure socket.", e);
        }
    }

    public boolean isOpen() {
        if (socket_ == null) {
            return false;
        }

        return socket_.isConnected();
    }

    public void open() {
        checkOpenParam();

        if (socket_ == null) {
            initSocket();
        }

        try {
            socket_.connect(new InetSocketAddress(host_, port_), connectTimeout_);
            inputStream_ = new BufferedInputStream(socket_.getInputStream());
            outputStream_ = new BufferedOutputStream(socket_.getOutputStream());

            protocol = new SimpleProtocol(inputStream_, outputStream_);

        } catch (IOException e) {
            close();
            throw new RuntimeException("not connect to server");
        }

    }

    private void checkOpenParam() {
        if (isOpen()) {
            throw new RuntimeException("Socket already connected.");
        }

        if (host_ == null || host_.length() == 0) {
            throw new RuntimeException("Cannot open null host.");
        }

        if (port_ <= 0 || port_ > 65535) {
            throw new RuntimeException("Invalid port " + port_);
        }
    }

    /**
     * Sets the socket timeout and connection timeout.
     *
     * @param timeout Milliseconds timeout
     */
    public void setTimeout(int timeout) {
        this.setConnectTimeout(timeout);
        this.setSocketTimeout(timeout);
    }

    /**
     * Sets the socket timeout
     *
     * @param timeout Milliseconds timeout
     */
    public void setSocketTimeout(int timeout) {
        socketTimeout_ = timeout;
        try {
            socket_.setSoTimeout(timeout);
        } catch (SocketException e) {
            LOGGER.warn("Could not set socket timeout.", e);
        }
    }

    /**
     * Sets the time after which the connection attempt will time out
     *
     * @param timeout Milliseconds timeout
     */
    public void setConnectTimeout(int timeout) {
        connectTimeout_ = timeout;
    }

    public void close() {
        // Close the underlying streams
        try {
            if (inputStream_ != null) {
                try {
                    inputStream_.close();
                } catch (IOException e) {
                    LOGGER.warn("Error closing input stream.", e);
                }
            }

            if (outputStream_ != null) {
                try {
                    outputStream_.close();
                } catch (IOException e) {
                    LOGGER.warn("Error closing input stream.", e);
                }
            }
        } finally {
            inputStream_ = null;
            outputStream_ = null;
        }

        if (socket_ == null) {
            return;
        }

        // Close the socket
        try {
            socket_.close();
        } catch (IOException e) {
            LOGGER.warn("Could not close socket.", e);
        }
        socket_ = null;
    }

    public InputStream getInputStream_() {
        return inputStream_;
    }

    public OutputStream getOutputStream_() {
        return outputStream_;
    }

    public void write(String content) throws IOException {
        protocol.write(content);
    }

    public String read() throws IOException {
        return protocol.read();
    }
}
